Skip to content

fix(files): image drag-reorder move + unwedge post-stream editor + bullet/image Backspace fixes#5608

Merged
waleedlatif1 merged 4 commits into
stagingfrom
fix-image-selection-fresh
Jul 11, 2026
Merged

fix(files): image drag-reorder move + unwedge post-stream editor + bullet/image Backspace fixes#5608
waleedlatif1 merged 4 commits into
stagingfrom
fix-image-selection-fresh

Conversation

@waleedlatif1

@waleedlatif1 waleedlatif1 commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Drag-reorder of an image now actually moves it (reported on latest staging, deploy verified via CodePipeline: dragging duplicated the image and left the original; a click with a few px of hand jitter — which the draggable <img> turns into a native drag+drop-on-self — killed the ring and duplicated too, reading as "I can't select this image"). Root causes empirically pinned: TipTap node-view dragstart bypasses PM's drag serialization entirely (verified in @tiptap/core source — no PM text/html, no view.dragging; it does NodeSelect the node), so the drop carries only the browser's native enrichment whose <img src> is the ABSOLUTE rendered URL — which both hosted-image recognizers rejected. Fix: origin-aware src normalization (toSameOriginPath, deliberately window.location.origin — the browser serializes against the actual viewing origin, not the configured NEXT_PUBLIC_APP_URL) + handleDrop performs the internal move itself when the drop's html references the currently-selected image node (PM's default would parse-copy with the display-layer src and never delete the original). Drop-on-self is a no-op that keeps the selection ring. The paste-clone path had the same absolute-URL gap for browser-native "Copy Image" — also fixed.
  • Post-stream reconcile can no longer wedge the editor read-only: the content query polls while reconciling (fast 1.5s cadence for 45s, then degrades to 15s — never stops outright), exiting the moment a fetch shows the server content advanced. Empirically reproduced pre-fix (editor stuck contenteditable=false forever with the server holding the new content) and verified recovering in one poll post-fix.
  • Backspace on an empty sole bullet no longer destroys the image below it: the post-delete selection is now always a caret (previous textblock, else a gap cursor at the deletion point) — Selection.near used to silently NodeSelect the following image, making the next keystroke delete/replace it. The regression test also surfaced two pre-existing gap-cursor Backspace crashes (ours and TipTap's blockquote handler), both fixed.

Type of Change

  • Bug fix

Testing

  • Real-Chromium harness driving the real RichMarkdownEditor + engine (network stubbed with an in-memory server mirroring the real save→invalidate→refetch dynamics): drag-reorder before/after (duplicate+no-move → clean single move, stays selected), real-mouse micro-drag gesture proof (becomes native drag, never a click), reconcile wedge before/after, paste-clone for relative and absolute payloads, plus regression sweeps over fresh-upload/click/focus-refetch/save-cycle scenarios.
  • 604 unit tests pass (11 new for origin-aware helpers, 3 for reconcile polling through the real engine, 3 for the refetchInterval passthrough against real react-query, 4 keymap regressions) — the new ones verified to fail pre-fix. tsc, biome, api-validation clean.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

…troying the image below it

Reported: clearing an empty bullet with an image after it "nuked the bullet point and the image".
Reproduced: when the emptied bullet is the doc's first block, removeEmptyWrappedBlock's
Selection.near(resolve(start), -1) finds no text position behind it and silently lands a
NodeSelection on the FOLLOWING image — so the user's next keystroke is destructive (a second
Backspace while clearing deletes the image; typing replaces it). Only-first-block explains why it
wouldn't re-repro. The selection left behind is now always a caret: end of the previous textblock
first, else a gap cursor at the deletion point when the neighbour is a leaf (typing there inserts a
new block instead of replacing the image), else the next textblock.

The regression test surfaced two adjacent gap-cursor crashes on Backspace, both reachable on
current staging whenever a gap cursor exists (e.g. between two dividers/images, the
data-gap-between-leaves state):
- our own handler threw RangeError from $from.before(0) on a depth-0 (doc-start) gap cursor
- with that guarded by falling through, TipTap's blockquote Backspace handler crashes on the same
  resolution ($from.node(-1) is undefined) — so a doc-start gap cursor consumes the key instead
  (there is nothing before it for Backspace to act on)
…its, so the editor can't wedge read-only

Reported: images "don't get selected sometimes" (can't grab/drag/resize, doc uneditable) until a
full refresh. Reproduced in a real-Chromium harness driving the actual RichMarkdownEditor + engine:
after an agent stream settles, the reconcile phase exits only when a fetch shows the server content
advanced past the pre-stream baseline — but that exit had no retry. A single refetch racing the
agent's write (or the mothership invalidation never reaching this surface — it's the only place
that invalidates this query) left the editor locked read-only indefinitely: contenteditable=false,
images not grabbable, until refetchOnWindowFocus or a reload happened to run.

Fix: while (and only while) the reducer is in `reconciling`, the content query polls via
react-query's refetchInterval — the same pattern this module already uses for the generated-doc
409 polling, and like it, bounded (45s window; past that the write has almost certainly failed and
refetchOnWindowFocus remains the recovery). The interval is the function form reading the phase
through a ref, re-evaluated by react-query after every fetch, so polling stops the moment a fetch
advances without needing an extra render.

Harness (real Chromium, real editor + engine, in-memory server): pre-fix the editor stays
editable=false with fetches frozen at 1 indefinitely while the server holds the new content;
post-fix it unlocks within one poll (~1.5s), polling stops immediately after finalize, and the
streamed image click-selects. Unit tests drive stream -> settle -> advance through the real engine
and assert the interval flips on/off with the phase (2 of 3 fail pre-fix), plus the bounded window
and a no-polling guard for plain at-rest editing.
@vercel

vercel Bot commented Jul 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jul 11, 2026 11:36pm

Request Review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor

cursor Bot commented Jul 11, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Reconcile polling touches the shared editable-file path and react-query behavior after agent streams; markdown keymap changes affect Backspace on lists and gap cursors. Brex change is a small response-field fix.

Overview
Fixes post-agent-edit wedging by polling workspace file content while the editor is in the reconciling phase: useWorkspaceFileContent now accepts refetchInterval (including a function re-evaluated by react-query), and useEditableFileContent drives 1.5s polling for 45s, then 15s until a fetch shows content past the pre-stream baseline—then unlocks and stops polling. Exposes isReconciling internally for that gate; new hook-level and react-query passthrough tests cover the behavior.

Rich markdown Backspace after removing an empty list/blockquote item no longer leaves a NodeSelection on a neighboring image/divider (removeEmptyWrappedBlock prefers prior textblock, then gap cursor via isLeafGap, then forward search). Gap cursor at doc start (depth === 0) is handled by consuming Backspace so custom and TipTap handlers don’t throw. Several keymap regression tests added.

Brex Get Company maps account_type from the API (was wrong camelCase key); block output descriptions for description/amount mention transfers.

Reviewed by Cursor Bugbot for commit 28fdba0. Configure here.

@greptile-apps

greptile-apps Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes several file-editor edge cases. The main changes are:

  • Image drag-reorder now moves the selected image instead of duplicating it.
  • Post-stream reconciliation now keeps polling until server content advances.
  • Backspace around empty bullets and adjacent images now leaves a non-destructive selection.
  • Brex metadata and company account type mapping were adjusted.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/use-editable-file-content.ts Adds reconcile polling with fast and slow cadences so the editor can unlock after delayed server updates.
apps/sim/hooks/queries/workspace-files.ts Passes optional polling settings through to the workspace file content query.
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/keymap.ts Updates Backspace selection handling around empty wrapped blocks and gap cursors.

Reviews (4): Last reviewed commit: "fix(files): degrade reconcile polling to..." | Re-trigger Greptile

@greptile-apps

greptile-apps Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes two editor interaction paths around streamed file content and Backspace handling. The main changes are:

  • Bounded polling while post-stream content is reconciling.
  • A new query option for file-content refetch intervals.
  • Safer Backspace handling for empty wrapped blocks and gap cursors.
  • Regression tests for reconcile polling and rich-markdown keymap behavior.

Confidence Score: 4/5

The changed keymap path needs a fix before merging.

  • Reconcile polling is scoped to the reconciling phase and has a bounded timeout.
  • Existing file-content query callers remain compatible with the new optional argument.
  • Deleting an empty wrapper after a selectable leaf can still leave that leaf selected, making the next keystroke destructive.

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/keymap.ts

Important Files Changed

Filename Overview
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/keymap.ts Adds safer selection recovery and gap-cursor Backspace handling, but the backward fallback can still select a preceding leaf.
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/keymap.test.ts Adds regression tests for image preservation and gap-cursor Backspace crashes.
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/use-editable-file-content.ts Adds a bounded refetch interval while streamed content waits for server reconciliation.
apps/sim/hooks/queries/workspace-files.ts Adds an optional refetchInterval passthrough to the workspace file content query.
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/use-editable-file-content.test.tsx Adds hook tests for reconcile polling start, stop, timeout, and at-rest behavior.

Reviews (2): Last reviewed commit: "fix(files): poll the content query while..." | Re-trigger Greptile

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit adcbe0a. Configure here.

…-query

Both consumers' test setups (the reconcile unit tests and the browser harness) replace
@/hooks/queries/workspace-files, so the real hook's two changed lines were exercised by nothing but
the type-checker. These render the real useWorkspaceFileContent under a real QueryClientProvider
with a stubbed fetch: no polling by default, polling with a numeric interval, and the function form
re-evaluated so flipping its condition stops the polling — the exact mechanism the reconcile fix
depends on. The two polling tests fail against the pre-fix hook.
…opping; prove findFrom textOnly never leaf-selects

Greptile round 1:
- Real gap in my bounded window: past 45s the poll stopped outright, leaving the reducer wedged in
  reconciling with only focus-refetch/reload as recovery — the exact failure shape this PR exists
  to remove, just later. Polling now degrades to a 15s cadence instead of stopping, so a write
  landing late (slow job, replica catch-up) is still picked up automatically; react-query pauses
  interval refetches in background tabs by default, so an abandoned doc doesn't poll unattended.
- Refuted with source + an executable test: Selection.findFrom($gap, -1, true) cannot return a
  NodeSelection — prosemirror-state's findSelectionIn skips atoms entirely under textOnly
  (`!text && isSelectable`). New regression test pins the exact scenario (image directly BEFORE the
  emptied bullet): backward search returns null, the gap-cursor/forward-caret branches take over,
  and the image is never silently selected.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 28fdba0. Configure here.

@waleedlatif1 waleedlatif1 merged commit 0aa2309 into staging Jul 11, 2026
18 checks passed
@waleedlatif1 waleedlatif1 deleted the fix-image-selection-fresh branch July 11, 2026 23:56
@waleedlatif1 waleedlatif1 changed the title fix(files): unwedge post-stream read-only editor; stop bullet Backspace from destroying the image below fix(files): image drag-reorder move + unwedge post-stream editor + bullet/image Backspace fixes Jul 12, 2026
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 28fdba0. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant